
#
# A Production Rule System implemented in Python.
#
# (C) Copyright 2006-2008, Johan Lindberg
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 59 Temple
# Place, Suite 330, Boston, MA 02111-1307 USA
#

import logging
import time

import conflictresolver
import executionenvironment
import rete
import rulebase
import rulecompiler
import workingmemory

WATCH_ITEMS = ['facts', 'rules', 'statistics', 'focus', 'compilations']

class RuleEngine(object):
    def __init__(self, env, test = False):
        """
        The RuleEngine class provides the public API for pyRete. Most, if
        not all, interactions with the Rule Engine should go through this
        class.
        """
        
        self.env = env
        self.test = test
        
        self.conflict_resolver = conflictresolver.ConflictResolver(self)
        self.rule_base = rulebase.RuleBase(self)
        self.working_memory = workingmemory.WorkingMemory(self)
        self.execution_environment = executionenvironment.ExecutionEnvironment(self)
        
        self._rete = rete.Rete(self)
        self._rule_compiler = rulecompiler.RuleCompiler(self, self._rete)
        
        self.watch('compilations') # default watch items
        
        self.logger = logging.getLogger('PyRete')
        
        
    ### Published (User) API
    ### ====================
    ###
    ### Environment operations
    ### ----------------------
    def load(self):
        """
        """
        self.logger.info("load")
        
    def reset(self):
        """
        Reset clears the working memory, the list of fired activations and
        the agenda.
        """
        
        self.logger.info("reset")
        
        self.working_memory.reset()
        self.conflict_resolver.reset()
        
    def run(self, limit = -1, rule = None):
        """
        Create an agenda for rule and execute activations until the agenda
        is empty or until limit activations have been executed.
        """
        
        self.logger.info("run(limit = %s, rule = %s)", limit, rule)
        
        start = time.time()
        self._execute_agenda(limit, rule)
        
        stop = time.time()
        
        
        
    def set_strategy(self, strategy):
        """
        Sets the conflict resolution strategy.
        """
        
        self.logger.info("set_strategy(strategy = %s)", strategy)
        self.conflict_resolver.strategy(strategy)
        
    def get_strategy(self):
        """
        Get the current conflict resolution strategy.
        """
        
        self.logger.info("get_strategy() -> %s", self.conflict_resolver.strategy)
        return self.conflict_resolver.strategy
        
        
    ### Debugging operations
    ### --------------------
    def agenda(self):
        """
        """
        pass
        
        
    def focus(self, *modules):
        """
        """
        pass
        
    def get_focus(self):
        """
        """
        pass
        
        
    def watch(self, *items):
        """
        """
        pass
        
    def unwatch(self, *items):
        """
        """
        pass
        
    def get_watch_items(self):
        """
        """
        pass
        
        
    def matches(self, rule):
        """
        """
        pass
        
        
    def set_break(self, *rules):
        """
        """
        pass
        
    def remove_break(self, *rules):
        """
        """
        pass
        
    def get_breaks(self):
        """
        """
        pass
        
        
    ### Fact operations
    ### ---------------
    def assert_fact(self, *facts, **kwargs):
        """
        """
        
        for fact in facts:
            self._rete.add_object(fact)
            
    def duplicate_fact(self, *facts, **attrs):
        """
        """
        pass
        
    def modify_fact(self, *facts, **attrs):
        """
        """
        pass
        
    def retract_fact(self, *facts):
        """
        """
        for fact in facts:
            self._rete.del_object(fact)
            
            
    ### Fact Query operations
    ### ---------------------
    def compile_query(self, expression):
        """
        """
        pass
        
    def any(self, expression, compile = True):
        """
        """
        pass
        
    def find(self, expression, compile = True):
        """
        """
        pass
        
    def do(self, expression, callable, compile = True):
        """
        """
        pass
        
    def do_for_all(self, expression, callable, compile = True):
        """
        """
        pass
        
        
    ### Rule operations
    ### ---------------
    def add_rule(self, *rules):
        """
        Compile and add each of the functions in rules to the Rete Network.
        
        Once a function has been compiled it is replaced by a function that
        raises an Exception when it's called directly.
        """
        
        def f(*args, **kwargs):
            raise Exception("You cannot call a compiled rule as if it was a regular function!")
            
        for rule in rules:
            self._rule_compiler.compile(rule)
            self.env[rule.func_name] = f
            
    def remove_rule(self, *rules):
        """
        """
        pass
        
        
    ### Module operations
    ### -----------------
    def add_module(self, *modules):
        """
        """
        pass
        
    def remove_module(self, *modules):
        """
        """
        pass
        
        
    ### Private API
    ### ===========
    ###
    ### Environment operations
    ### ----------------------
    def _get_conflict_set(self, rule = None):
        return self._rete.get_conflict_set(rule)
        
    def _make_agenda(self, rule = None):
        """
        Make an Agenda.
        
        If rule is None an Agenda containing activations for all rules is
        created otherwise only the activations for rule is considered.
        """
        
        return self.conflict_resolver.resolve(self._get_conflict_set(rule))
        
        
    def _execute_agenda(self, limit = -1, rule = None):
        """
        Trigger, at most, limit activations on agenda. If limit
        is None all activations on agenda are triggered.
        """
        
        for rule, salience, token, rhs, locals in self._make_agenda(rule):
            if limit != 0:
                self.logger.info("Fire: %s (%s)", rule, ",".join(token.keys()))
                self.execution_environment.execute_activation((rule, salience, token, rhs, locals))
                limit -= 1
                
def module(name):
    """
    This decorator is used to specify which module a Rule should belong
    to. <name> should be a String with the module's name.
    """
    def deco(function):
        function._module = name
        return function
        
    return deco
    
    
import inspect
def rule(function):
    """
    This decorator is used to turn a Python function into a PyRete rule.
    
    The template code for a Rule is as follows:
    
    @rete.rule()
    @rete.module(<module name>)?
    def <rule name>(<engine>, <object binding>+):
      <rule property>*
      <local variable>*
      if <LHS>:
        <RHS>
        
    where <module name> is a string indicating which module this rule
    belongs to.
    
    <engine> is the name of the variable that refers to the RuleEngine
    object.
    
    An <object binding> is defined as if it was a keyword parameter:
    
    <variable name> = <class name> # class name of a Fact object
    
    there must be, at least, one binding for each rule.
    
    <rule property> is either, or both, of:
    
    salience = <expression>   # must return an int
    auto_focus = <expression> # must return a boolean
    
    You can define one or more local variables in the rule as you would
    define them in a regular function:
    
    <variable name> = <expression>
    
    variables can be used in both the LHS and the RHS of a rule. The only
    restriction (apart from the ones imposed by Python) is that you cannot
    use salience or auto_focus as variable names. They will be interpreted
    as rule properties.
    
    The LHS must consist of at least one match expression but may contain
    any number of them in combination with any number of constraints:
    
    <match expression> [and <match expression>|<constraint>]*
    
    The RHS can be any number of Python statements and/or expressions but
    you cannot define new rules, facts and/or functions and you cannot
    "return" a value.
    """
    
    try:
        function._source = inspect.getsource(function)
        
    except IOError:
        raise Exception("Cannot find source for function '%s'" % (function.__name__))
        
    return function
    
    
def shell():
    """
    """
    import console
    console.interact(Engine())
    
    
def run_tests():
    """
    """
    import doctest
    doctest.testmod()
    
    doctest.testmod(conflictresolver)
    doctest.testmod(rete)
    doctest.testmod(rulebase)
    doctest.testmod(rulecompiler)
    doctest.testmod(workingmemory)
    
    
if __name__ == '__main__':
    run_tests()
    